package fr.bludwarf.skywarrior.global;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import fr.bludwarf.commons.xml.ElementXML;
import fr.bludwarf.skywarrior.evenements.Cycle;
import fr.bludwarf.skywarrior.evenements.Evenement;
import fr.bludwarf.skywarrior.exceptions.EvenementInconnuOublieException;
import fr.bludwarf.skywarrior.joueur.Vaisseau;

// FIXME : créer un objet pour éviter tous les "_tempsRestants *= timeFactor" du code !!!
public class Temps {
	
	/** Format d'affichage des dates/heures réelles */
	private static final SimpleDateFormat SDF = new SimpleDateFormat("HH:mm [dd]");
	
	/** Durée d'un cycle en secondes */
	public static final int DUREE_CYCLE = 7 * 60 + 30;
	
	/** Temps infini pour un évènement */
	public static final double INFINI = -1;
	
	public static Map<Integer, Double> TIME_FACTORS = new HashMap<Integer, Double>();
	
	/** Temps écoulée depuis le début de la partie en secondes */
	double _tempsEcoule = 0;
	
	/** Date et heure réelle du début du temps */
	final Calendar _debut;
	
	
	static
	{
		TIME_FACTORS.put( 3, 1.6262);	//  1 x Ballon
		
		TIME_FACTORS.put( 8, 1.0);
		
		TIME_FACTORS.put(17, 0.853907);	// 10 x Ballon
		TIME_FACTORS.put(18, 0.533333);	//  1 x Harpie
		
		TIME_FACTORS.put(35, 0.430235);	//  2 x Atlas
		TIME_FACTORS.put(36, 0.424252);	//  2 x Atlas
		TIME_FACTORS.put(37, 0.418483);	//  2 x Atlas
		TIME_FACTORS.put(38, 0.412927);	//  2 x Atlas
	}
	
	
	public Temps(final Calendar debut) {
		_debut = debut;
	}
	
	public Temps(final Calendar debut, double tempsEcoule) {
		_debut = debut;
		_tempsEcoule = tempsEcoule;
	}

	/**
	 * @param debut
	 */
	public Temps(Date debut, double tempsEcoule)
	{
		final Calendar cal = Calendar.getInstance();
		cal.setTime(debut);
		_debut = cal;
		_tempsEcoule = tempsEcoule;
	}

	/**
	 * @param debut
	 */
	public Temps(Date debut, Date dateActuelle)
	{
		final Calendar cal = Calendar.getInstance();
		cal.setTime(debut);
		_debut = cal;
		_tempsEcoule = Temps.tempsEcoule(debut, dateActuelle);
	}

	/**
	 * @param debut
	 */
	public Temps(Calendar debut, Date dateActuelle)
	{
		this(debut.getTime(), dateActuelle);
	}
	
	public Temps(Temps tempsInitial, double temps) {
		_debut = tempsInitial._debut;
		_tempsEcoule = tempsInitial._tempsEcoule + temps;
	}

	public Temps()
	{
		this(Calendar.getInstance());
	}

	public Temps(String dateXML) throws Exception
	{
		this(ElementXML.DATE_TRANSFORM.read(dateXML), 0);
	}

	@Override
	public String toString() {
		return toString(_tempsEcoule);
	}

	/**
	 * @return date réelle à cet instant à partir du début réel de la partie et du temps écoulé depuis
	 */
	public Calendar getDateReelle() {
		final Calendar date = (Calendar) _debut.clone();
		date.add(Calendar.SECOND, (int) Math.round(_tempsEcoule));
		return date;
	}
	
	public String formatDateReelle()
	{
		final Calendar cal = arrondiMinute(getDateReelle());
		return formatDateReelle(cal.getTime());
	}
	
	public static String formatDateReelle(final Date date)
	{
		return SDF.format(date);
	}

	/**
	 * @param date TODO
	 * @return
	 */
	protected Calendar arrondiMinute(Calendar date)
	{
		final Calendar cal = Calendar.getInstance();
		cal.setTime(date.getTime());
		if (cal.get(Calendar.SECOND) >= 30)
		{
			cal.add(Calendar.MINUTE, 1);
		}
		return cal;
	}
	
	@Override
	protected Object clone() {
		final Temps temps = new Temps(this._debut);
		temps._tempsEcoule = this._tempsEcoule;
		return temps;
	}

	public void avancer(final double temps) {
		_tempsEcoule += temps;
	}
	
	/**
	 * @return période restante du prochain cycle
	 */
	public Evenement prochainCycle(final EtatMonde etatMonde)
	{
		// 16/12/13 : Apparamment le cycle est calculé de façon absolu (même si la partie n'a pas démarrée) 
		// aujourd'hui un cycle a été observé à 11:19:48 : soit modulo 7:30 (450) = 0
		// donc il suffit juste de faire currentTime % 450
		
		// nouveau cycle : 11:34:48
		
		final Calendar cal = getDateReelle();
		final long timeInMillis = cal.getTimeInMillis();
		final long mod = timeInMillis % (long)DUREE_CYCLE;
		return new Cycle(DUREE_CYCLE - mod);
//		return new Cycle(tempsAvantProchainCycle());
	}
	
	/**
	 * @return Temps restant avant le prochain cycle
	 * @since 17 nov. 2011
	 */
	public double tempsAvantProchainCycle()
	{
		return DUREE_CYCLE - _tempsEcoule % DUREE_CYCLE;
	}
	
	/**
	 * Permet d'afficher un temps sous forme lisible
	 * @param temps temps en secondes
	 */
	public static String toString(final double temps)
	{
		return Temps.timeToString(temps);
	}

	public double tempsEcouleDepuis(Temps temps) {
		return this._tempsEcoule - temps._tempsEcoule;
	}

	public double tempsEcouleDepuis(Calendar cal)
	{
		return tempsEcouleDepuis(new Temps(cal));
	}

	public static double tempsEcoule(final Calendar debut, final Calendar fin) {
		return (double) (fin.getTimeInMillis() - debut.getTimeInMillis()) / 1000;
	}

	public static double tempsEcoule(Temps t1, Temps t2) {
		return t2._tempsEcoule - t1._tempsEcoule;
	}

	public static double tempsEcoule(Date debut, Date fin)
	{
		final Calendar cal = Calendar.getInstance();
		cal.setTime(debut);
		final Calendar cal2 = Calendar.getInstance();
		cal2.setTime(fin);
		return tempsEcoule(cal, cal2);
	}
	
	/**
	 * Calcul le facteur d'accélération ou de ralentissement des temps de construction
	 * @param oldTime temps de construction d'un élément (ou même {@link #getInitialNextPop()}
	 * @return facteur d'accélération ou de ralentissement des temps de construction. Valeurs possibles :
	 * <ul>
	 * 	<li>&lt; 1 => accélération
	 * 	<li>= 1 => aucun changement
	 * 	<li>&gt; 1 => ralentissement
	 * </ul>
	 * @since 9 nov. 2011
	 */
	public static double getTimeFactor(final double oldTime, final double newTime)
	{
		return newTime / oldTime;
	}
	
	/**
	 * Calcul le facteur d'accélération ou de ralentissement des temps de construction
	 * @param oldPop ancienne population
	 * @return newPop nouvelle population
	 * <ul>
	 * 	<li>&lt; 1 => accélération
	 * 	<li>= 1 => aucun changement
	 * 	<li>&gt; 1 => ralentissement
	 * </ul>
	 * @since 24 nov. 2011
	 */
	public static double getTimeFactor(final int oldPop, final double newPop)
	{
		return Math.sqrt(oldPop) / Math.sqrt(newPop);
	}

	/**
	 * Calcul le facteur d'accélération ou de ralentissement des temps de construction
	 * @param oldNourriture ancienne nourriture
	 * @return newNourriture ancienne nourriture
	 * @return pop population actuelle
	 * <ul>
	 * 	<li>&lt; 1 => accélération
	 * 	<li>= 1 => aucun changement
	 * 	<li>&gt; 1 => ralentissement
	 * </ul>
	 * @since 24 nov. 2011
	 */
	public static double getTimeFactor(int oldNourriture, int newNourriture, int pop) {
		return (double) (oldNourriture - pop) / (newNourriture - pop);
	}

	/**
	 * @param pop
	 * @return TimeFactor pour un certain type
	 * @throws TimeFactorInconnu si le TimeFactor est inconnu pour le type demandé
	 * @since 10 nov. 2011
	 */
	public static double getTimeFactor(Class<?> clazz, int pop, final boolean architecte) throws TimeFactorInconnu
	{
		// La formule du TimeFactor est toujours inconnue alors si on connait déjà la valeur pour la pop on la récupère
		// sinon on calcul une approximation
		
		double tf;
		
		if (TIME_FACTORS.containsKey(pop))
			tf = TIME_FACTORS.get(pop);
		else
			tf = Math.sqrt((double) 8 / pop);
		
		if (clazz == Vaisseau.class)
		{
			// FIXME : Time Factor Vaisseau : envoit d'exception si timeFactor inconnu
			if (architecte)
			{
				tf *= 0.8;
			}
			return tf;
		}
		else
		{
			if (architecte)
			{
				tf *= 0.7;
			}
			return tf;
		}
	}

	public static double applyTimeFactor(double factor, double tempsRestant) {
		if (tempsRestant == INFINI)
		{
			return INFINI;
		}
		else
		{
			return factor * tempsRestant;
		}
	}

	public static double decrementer(double tempsRestant, double temps) throws EvenementInconnuOublieException {
		if (tempsRestant == INFINI)
		{
			return INFINI;
		}
		else if (tempsRestant >= temps)
		{
			return Math.max(0, tempsRestant - temps);
		}
		else
		{
			throw new EvenementInconnuOublieException(String.format(
				"Impossible de décrementer %s de %s car un évènement inconnu a été oublié",
				Temps.timeToString(tempsRestant),
				Temps.timeToString(temps)
				));
		}
	}

	/**
	 * @return date/heure + temps écoulé
	 * @since 10 nov. 2011
	 */
	public String toCompleteString() {
//		return formatDateReelle() + " (t = " + toString(_tempsEcoule) + ")";
		return formatDateReelle();
	}

	public double tempsPourCollecter(int cout, int prod, int stocks) {
		// si stocks suffisants
		if (stocks >= cout)
		{
			return 0;
		}
		
		// si production insuffisante
		else if (cout != 0 && prod == 0)
		{
			return Temps.INFINI;
		}
		else
		{
			// Temps écoulé depuis le dernier cycle
			final double offset = DUREE_CYCLE - tempsAvantProchainCycle();
			final int nbCycles = (int) Math.ceil((double) (cout - stocks) / prod);
			return nbCycles * DUREE_CYCLE
				- offset; // arrondi au cycle précédent (pas besoin d'attendre plus)
		}
	}

	/**
	 * <b>Arrondi à la seconde la plus proche</b>
	 * @param duree
	 * @return
	 * 
	 * @see #parseDuree(String)
	 */
	public static String formatDuree(double duree)
	{
		final long dureeInt = Math.round(duree);
		
		if (dureeInt == INFINI || Double.isInfinite(dureeInt))
			return "INF";
		
		if (dureeInt <= 0)
			return "0s";
		
		final long m = (dureeInt % 3600) / 60;
		final long s = dureeInt % 60;
		final long h = dureeInt / 3600;
		
		final StringBuilder sb = new StringBuilder();

		if (h > 0)
			sb.append(h).append('h');
		
		if (m > 0)
			sb.append(" ").append(m).append('m');
		
		if (s > 0)
			sb.append(" ").append(s).append('s');

		return sb.toString().trim();
	}

	/**
	 * @param duree
	 * @return
	 * 
	 * @see #formatDuree(double)
	 */
	public static double parseDuree(String duree)
	{
		if (duree.equals("INF"))
			return INFINI;
		
		Pattern p = Pattern.compile("(?:(\\d+)h)? *(?:(\\d+)m)? *(?:(\\d+)s)?");
		Matcher m = p.matcher(duree);
		
		int sec = 0;
		
		if (m.find())
		{
			if (m.group(1) != null)
			{
				sec += Integer.parseInt(m.group(1)) * 3600;
			}
			if (m.group(2) != null)
			{
				sec += Integer.parseInt(m.group(2)) * 60;
			}
			if (m.group(3) != null)
			{
				sec += Integer.parseInt(m.group(3));
			}
		}
		
		else
		{
			p = Pattern.compile("(\\d+):(\\d+):(\\d+)");
			m = p.matcher(duree);
			
			if (m.find())
			{
				sec = Integer.parseInt(m.group(1)) * 3600;
				sec += Integer.parseInt(m.group(2)) * 60;
				sec += Integer.parseInt(m.group(3));
			}
			
			else
			{
				throw new RuntimeException("Impossible de parser la durée " + duree);
			}
		}
		
		return sec;
	}

	public static String timeToString(final int nbJours, final int hours, final int mins, final int sec)
		{
	//		final StringBuilder sb = new StringBuilder();
	//		
	//		if (nbJours > 0)
	//		{
	//			sb.append(nbJours).append("j ");
	//		}
	//		if (hours > 0)
	//		{
	//			sb.append(hours).append("h ");
	//		}
	//
	//		sb.append(pad2(mins)).append("m ").append(pad2(sec)).append("s");
	
			return String.format("%02d:%02d:%02d", hours + nbJours * 24, mins, sec);
		}

	public static String timeToString(final double seconds)
	{
		final Calendar date0 = newTime();
		final Calendar date = newTime();
		
		// Nombre de secondes
		date.set(Calendar.SECOND, (int) Math.round(seconds));
		
		// Nombre de jours
		final double diff = date.getTimeInMillis() - date0.getTimeInMillis();
		final int nbJours = (int) (diff / (24 * 3600 * 1000));
		
		return timeToString(nbJours, date.get(Calendar.HOUR_OF_DAY), date.get(Calendar.MINUTE), date.get(Calendar.SECOND));
	}

	public static Calendar newTime()
	{
		final Calendar date = Calendar.getInstance();
		date.set(Calendar.HOUR, 0);
		date.set(Calendar.HOUR_OF_DAY, 0);
		date.set(Calendar.MINUTE, 0);
		date.set(Calendar.SECOND, 0);
		date.set(Calendar.MILLISECOND, 0);
		return date;
	}

	public static String pad2(final int i)
	{
	    if (i < 10)
	    {
	        return "0" + i;
	    }
	    else
	    {
	        return Integer.toString(i);
	    }
	}

	public static double applyTimeFactor(double ancienTempsTotal, double nouveauTempsTotal, double tempsRestant) {
		
		// FIXME : peut-être un problème de calcul, car tests KO
		return Temps.applyTimeFactor(getTimeFactor(ancienTempsTotal, nouveauTempsTotal), tempsRestant);
		
		// FIXME : cette fois le nextPop est un peu plus rapide que sur Skywar
		// On décrémente au nouveau nextPop le temps déjà passé sur le nextPop
//		return Math.max(nouveauTempsTotal - (ancienTempsTotal - tempsRestant), 0);
		
//		return nouveauTempsTotal;
	}

	public static String formatter(double seconds)
	{
		return Temps.timeToString(seconds);
	}
	
	public double getTempsEcoule()
	{
		return _tempsEcoule;
	}

	public Calendar getDebut()
	{
		return _debut;
	}
}
